import { Client, type IPublishParams } from '@stomp/stompjs';
import { dateUtils, numberUtils } from 'my-ts-core';
import type { StringKeyObject } from 'my-ts-core/types';
import type { WebSocketBody } from 'my-web-core/network';

/**
 * It handles stomp subscriptions and their status.
 */
export class StompSubscriber {
  private readonly clientOnDestinationPrefix: string = '/topic';

  // We have a public getter of id. That's why we name it start with underscore which follows official guide.
  // https://www.typescriptlang.org/docs/handbook/2/classes.html#getters--setters
  private _id: string;

  // A stomp client over a web socket connection.
  private stompClient: Client;

  // Stores all subscription ids and unsubscibes all in clear method.
  private subscriptionIds: StringKeyObject = {}; // string[] = [];

  // Nees a stomp client which already has a web socket connection.
  constructor(stompClient: Client) {
    this._id = `${dateUtils.getCurrentDate().getTime()}-${numberUtils.getRandomNumberWithinRange(100000, 999999)}}`;
    this.stompClient = stompClient;
  }

  get id(): string {
    return this._id;
  }

  /**
   * Subscribes destination over a web socket connection and resturn its id.
   * You can use this id to unsubscribe manually or use clear method to unsubcribe all existing subscriptions.
   * @param param Contains server destination path and header info.
   * @returns String of subscription id.
   */
  subscribe(param: {
    destination: string;
    callback?: (res: any) => void;
    headers?: StringKeyObject;
  }): string {
    const id = this.stompClient.subscribe(
      param.destination,
      (msg) => {
        param.callback?.call(this, JSON.parse(msg.body));
      },
      param.headers,
    ).id;

    // Stores the id for auto clear.
    this.subscriptionIds[id] = true;

    return id;
  }

  /**
   * Subscribes and returns data as promise. Unsubscribes automatically once the data returned.
   * @param param Contains server destination path and header info.
   */
  async subscribeOnce(param: {
    destination: string;
    headers?: StringKeyObject;
  }): Promise<StringKeyObject> {
    return new Promise((resolve) => {
      const id = this.stompClient.subscribe(
        param.destination,
        (msg) => {
          // Unsubscribes once data comes.
          this.unsubscribe(id);

          return resolve(JSON.parse(msg.body));
        },
        param.headers,
      ).id;

      this.subscriptionIds[id] = true;
    });
  }

  /**
   * Unsubscribes by id and header info.
   */
  unsubscribe(id: string, headers?: StringKeyObject): void {
    // Unsubscribes from remote server.
    this.stompClient.unsubscribe(id, headers);

    // Removes from local id list.
    delete this.subscriptionIds[id];
  }

  /**
   * Unsubcribes subscriptions based on ids and header info.
   */
  unsubscribeAll(ids: string[], headers?: StringKeyObject): void {
    for (const id of ids) {
      this.unsubscribe(id, headers);
    }
  }

  /**
   * Sends data to destination over a web socket connection.
   *
   * @param param Contains server destination path, header info and web socket body.
   */
  send(param: {
    destination: string;
    headers?: StringKeyObject;
    webSocketBody?: WebSocketBody;
  }): void {
    let publishParams: IPublishParams = {
      destination: param.destination,
      headers: param.headers,
    };

    if (param.webSocketBody) {
      publishParams.body = JSON.stringify(param.webSocketBody);
    }

    this.stompClient.publish(publishParams);
  }

  /**
   * Sends data to destination over a web socket connection and gets response right away.
   * @param param Contains server destination path, header info and web socket body.
   * @returns
   */
  async sendAndGet(param: {
    destination: string;
    headers?: StringKeyObject;
    webSocketBody?: WebSocketBody;
  }): Promise<StringKeyObject> {
    return new Promise((resolve) => {
      // Generates unique client on destination path based on server destination path.
      const clientOnDestination = `${this.clientOnDestinationPrefix}${param.destination}-${numberUtils.getRandomNumberWithinRange(100000, 999999)}`;

      // Creates subscription before sending.
      const promise = this.subscribeOnce({
        destination: clientOnDestination,
      });

      // Uses auto generated client on destination path for server sending back.
      param.webSocketBody ??= {};
      param.webSocketBody.clientOnDestination = clientOnDestination;
      this.send(param);

      return resolve(promise);
    });
  }

  /**
   * Unsubcribes all existing subscriptions.
   */
  clear(headers?: StringKeyObject): void {
    this.unsubscribeAll(Object.keys(this.subscriptionIds), headers);
  }
}
